#!/usr/bin/env python
"""
Example WSGI app utilized in signing ubiqis package metadata.

Configuration is done via a .ini file with a basename the same as this file.
It is advised to use absolute paths due to varying web-server configurations.

Example Config:
    [ubiqis_registrar]
        pubkey = /srv/ubireg/public
        privkey = /srv/ubireg/private.pem
        cache_dir = /srv/ubireg/

This file can be run as is and will start up a simple web server to process
the request on a specified port, defaulting to 8000.

"""

import anydbm
import hashlib
import os
import urllib2
import xml.etree.ElementTree as ET
try:
    import cpickle as pickle
except ImportError:
    import pickle

from ConfigParser import ConfigParser
from socket import getfqdn
from urllib import unquote
from wsgiref import simple_server, util as wsgiref_util

from Crypto.Util import number

import ubireg.keytool
import ubireg.names

# Load config from same basename as this file with .ini
config = ConfigParser()
config.read(os.path.splitext(__file__)[0] + '.ini')

_pub_key = config.get('ubiqis_registrar', 'pubkey')
_pub_key = ''.join(open(_pub_key, 'r').read().split('\n')[1:-2])
_priv_key = ubireg.keytool.key_from_file(config.get('ubiqis_registrar', 'privkey'))
_cache_dir = config.get('ubiqis_registrar', 'cache_dir')

def getpkg(url):
    """Fetches a ubiqis package from the web and returns an XML object"""
    if '.ubiq' in url:
        try:
            temp = urllib2.urlopen(url)
            content = temp.read()
        # TODO: Fall back to p2p
        except urllib2.URLError, message:
            return "<error>%s</error>" % message
        else:
            if temp.code != 200:
                return '<error>Error %s:%s</error>' % (temp.code, tmp.msg)
            if not '<ubiqis>' in content:
                return '<error>Not a valid .ubiq file</error>'
            tree = ET.fromstring(content)

            # Check to see if url matches given url:
            old_url = ubireg.names.pkg2url(tree.find('name').text)
            if old_url == url:
                return tree
            else:
                return '<error>Source URL does not match pkg URL</error>'
    else:
        return "<error>Invalid URL - Not a .ubiq file</error>"

def signpkg(tree, server):
    """Signs a ubiqfile and returns the xml in string form"""

    # Check to see if already signed
    sigs = tree.findall('signature')
    if sigs:
        for sig in sigs:
            tree.remove(sig)
    ubiq = ET.tostring(tree)
    # Create a hash of the original ubiq
    hash_obj = hashlib.new('sha1')
    hash_obj.update(''.join(ubiq.split()))

    # Sign the ubiq file
    signature = _priv_key.sign(hash_obj.hexdigest(), '')[0]
    signature = number.long_to_bytes(signature).encode('base64').split()
    signature = ''.join(signature)

    # Build the signature XML
    sig = ET.SubElement(tree, 'signature')

    # Add the elements
    host = ET.SubElement(sig, 'host') # Host element
    host.text = server
    type = ET.SubElement(sig, 'type') # hash type
    type.text = 'sha1WithRSAEncryption'
    signed_data = ET.SubElement(sig, 'signed-data') # signature
    signed_data.set('format', 'base64')
    signed_data.text = signature
    public_key = ET.SubElement(sig, 'public-key') # public key
    public_key.set('format', 'base64')
    public_key.text = _pub_key

    pprint(tree) # Indent the xml to proper depths

    for sig in sigs:
        tree.append(sig)

    return ET.tostring(tree)

def pprint(elem, level=0):
    """Indents XML to proper indent levels per child"""
    i = "\n" + level*"    "
    if len(elem):
        if not elem.text or not elem.text.strip():
            elem.text = i + "    "
        for child in elem:
            pprint(child, level+1)
        if not child.tail or not child.tail.strip():
            child.tail = i
        if not elem.tail or not elem.tail.strip():
            elem.tail = i
    else:
        if level and (not elem.tail or not elem.tail.strip()):
            elem.tail = i


def application(environ, start_response):

    # Set up keys and database store
    db = anydbm.open(os.path.join(_cache_dir, 'ubiqis.db'), 'c')

    def _bad_request():
        start_response("400 Bad Request", [('Content-Type', 'text/plain')])
        return ["400 Bad Request"]

    def _not_found():
        start_response("404 Not Found", [('Content-Type', 'text/plain')])
        return ["404 Not found"]

    try:
        app = wsgiref_util.shift_path_info(environ)

        # Root level, show nothing
        if app == '':
            start_response("200 OK", [('Content-Type', 'text/plain')])
            return ['']
        # Return signed package
        elif app in ('signpkg') and environ['REQUEST_METHOD'] == 'GET':
            if not environ['QUERY_STRING'].startswith('url='):
                _bad_request()
            input = environ['QUERY_STRING'][len('url='):]
            if input in db:
                # Return cached copy
                start_response("200 OK", [('Content-Type', 'text/xml')])
                return db[input]
            else:
                # Sign package
                tree = getpkg(input)
                if isinstance(tree, type('')):
                    start_response("200 OK", [('Content-Type', 'text/xml')])
                    return tree
                signed = signpkg(tree, environ['HTTP_HOST'])
                if not tree.findall('develop'):
                    # Cache package
                    db[input] = signed
                start_response("200 OK", [('Content-Type', 'text/xml')])

                # Enable key based search
                tree = ET.fromstring(signed)
                keys = tree.findall('key')
                for key in keys:
                    # prepending > prevents conflicts with ubi url keys
                    kdb = ">"+key.text
                    values = set()
                    if kdb in db:
                        values = pickle.loads(db[kdb])
                    values.add(input)
                    db[kdb] = pickle.dumps(values)

                return signed

        # Basic key search
        elif app in ('search'):
            if not environ['QUERY_STRING'].startswith('key='):
                return _bad_request()
            input = '>'+unquote(environ['QUERY_STRING'][len('key='):])
            start_response("200 OK", [('Content-Type', 'text/plain')])
            if input in db:
                return '\n'.join(pickle.loads(db[input]))+'\n'
                #return str(db[input])
            else:
                return 'None'

        # Return the public key used for signing
        elif app in ('getkey'):
            start_response("200 OK", [('Content-Type', 'text/plain')])
            return _pub_key
        else:
            return _not_found()
    finally:
        db.close()

if __name__ == '__main__':

    # Set up a basic webserver running the signing server
    import sys
    try:
        if len(sys.argv) > 1:
            port = int(sys.argv[1])
        else:
            port = 8000
        print "Running signing server at http://%s:%s" % (getfqdn(), port)
        httpd = simple_server.WSGIServer(('', port),
                                         simple_server.WSGIRequestHandler)
        httpd.set_app(application)
        httpd.serve_forever()
    except KeyboardInterrupt:
        print "\nQuitting..."
        exit(0)
